import {
  ApiV3PoolInfoStandardItem,
  AmmV4Keys,
  AmmRpcData,
  Raydium,
  API_URLS,
  ApiV3PoolInfoStandardItemCpmm,
  CpmmKeys,
  CpmmRpcData,
  CurveCalculator,
  ApiV3PoolInfoConcentratedItem,
  RAYMint,
  ClmmKeys,
  ComputeClmmPoolInfo,
  ReturnTypeFetchMultiplePoolTickArrays,
  PoolUtils,
  ApiV3PoolInfoItem
} from '@raydium-io/raydium-sdk-v2'
import {initSdk, owner, txVersion, amount, slippage, minSolBalance, connection, solPrice, maxMintTvl} from '../config'
import BN from 'bn.js'
import {isValidAmm, isValidCpmm, isValidClmm} from './utils'
import Decimal from 'decimal.js'
import { NATIVE_MINT ,getAssociatedTokenAddress} from '@solana/spl-token'
import { printSimulateInfo } from '../util'
import { PublicKey } from '@solana/web3.js'
import axios from 'axios'
import {LIQUIDITY_STATE_LAYOUT_V4, MAINNET_PROGRAM_ID} from "@raydium-io/raydium-sdk";

let raydium: Raydium = {} as Raydium

let mint :string = "" //新币token
let poolId:string = "" //交易对id
let solBalance:number = 0 //
let curProgramId:string = ""
let mintSupply:number = 0  //代币发行量
let mintDecimals:number = 9 //代币精度
let mintTvl = 0 //代币市值


export const main = async () => {
  raydium = await initSdk()

  const key2 = setInterval(async () => { //查询自己sol余额
    try {
      await querySolBalance(raydium)
    } catch (error) {
      console.log("======2222", error)
    }
  },  5000)

  const key = setInterval(async () => {
    try {
      if (!mint) {
        mint = await getNewToken(); //查询新币
      }
      if (mint) {
        console.log('mint is:', mint)
        // poolId = await getPoolId(raydium, mint); //查询交易对
        poolId = await getPoolId2(raydium, NATIVE_MINT.toBase58(), mint); //查询交易对
        console.log('poolId:', poolId)
      } else {
        console.log('未查询到新币')
      }
      if (poolId) {
        clearInterval(key)
      }
    } catch (error) {
      console.log("======", error)
    }
  },  1000)

  const key3 = setInterval(async () => { //查询代币供应量
    try {
      if (mint) {
        await queryTokenSupply(mint); //查询代币供应量
      }
      if (mintSupply && mintSupply > 0) {
        clearInterval(key3)
      }
    } catch (error) {
      console.log("======查询供应量报错", error)
    }
  },  1000)

  const key4 = setInterval(async () => { //查询代币市值
    try {
      if (mint) {
        await queryMintTvl(mint);
      }
    } catch (error) {
      console.log("======查询代币市值报错", error)
    }
  },  5000)


  const key1 = setInterval(async () => {
    try {
      await swap2()
    } catch (error) {
      console.log(error)
    }
  },  200)

}
export const swap2 = async () => {
  console.log(`开始执行swap交易...代币市值：${mintTvl}====代币要求的市值${maxMintTvl}`)

  if (solBalance > minSolBalance) {
    console.log(`当前余额${solBalance} 购买最低金额 ${minSolBalance}`)
    if (!poolId) {
      console.log("未找到交易对")
      return
    }
    if (mintTvl > maxMintTvl || mintTvl <= 0) {
      console.log("代币市值大于要求的最大值或者代币市值为0")
      return
    }
    console.log("开始执行swap交易", amount, poolId)
    await swap(raydium, amount, poolId)
  } else {
    console.log("账户余额不足", solBalance)
  }
}

export const getPoolId = async (raydium:Raydium, mint: string):Promise<string> => {
  const accounts = await raydium.connection.getProgramAccounts(
      // DEVNET_PROGRAM_ID.AmmV4,
      MAINNET_PROGRAM_ID.AmmV4,
      {
        filters: [
          {
            dataSize: LIQUIDITY_STATE_LAYOUT_V4.span
          },
          // 可以添加其他过滤条件，比如特定代币的池子
          {
            memcmp: {
              offset: LIQUIDITY_STATE_LAYOUT_V4.offsetOf('quoteMint'),
              bytes: mint // 比如只查USDC的池子  A28UET9gJsM8YXU9m3xaXcRskhUtP3xs2buYHeumoon
            }
          }
        ]
      }
  );
  //9o7KRNpjD5nnfSMyh5Cw8ULFUQXdKSSV894y6NG6Jf5E
  console.log(`找到 ${accounts.length} 个流动性池`);
  if (accounts && accounts.length == 1) {
    return accounts[0].pubkey.toBase58()
  }
  return "";
}

export const queryMintTvl = async (mint: string) => {
  const url = `${API_URLS.SWAP_HOST}/compute/swap-base-in?inputMint=So11111111111111111111111111111111111111112&outputMint=${mint}&amount=1000000000&slippageBps=${slippage * 100}&txVersion=V0`
  const data = await axios.get(url).then((res) => {
    return res.data
  }).catch((error) => {
    console.log("查询市值报错", error)
  })
  console.info("===查询市值数据是1111==", data)
  if (data && data.data && data.data.outputAmount) {
    mintTvl = Math.floor(mintSupply / (Math.floor(Number(data.data.outputAmount) / Math.pow(10, mintDecimals)))) * solPrice
  }
}




export const getPoolId2 = async (raydium:Raydium, inputMint: string, outputMint: string):Promise<string> => {
  const url = `${API_URLS.SWAP_HOST}/compute/swap-base-in?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amount}&slippageBps=${slippage * 100}&txVersion=V0`
  console.info("===查询的url==", url)
  const data = await axios.get(url).then((res) => {
    console.info("===查询的pool数据是==", res.data)
    return res.data
  }).catch((error) => {
    console.log("查询报错", error)
  })
  console.info("===查询的pool数据是1111==", data)
  if (data && data.data && data.data.routePlan && data.data.routePlan.length > 0) {
    console.info("===查询的poolid数据是222==", data.data.routePlan[0])
    return data.data.routePlan[0].poolId
  }
  return "";
}



export const getNewToken = async () :Promise<string> => {
  const response = await axios.get('http://127.0.0.1:9109/app/ton/getSolToken').then((res) => {
    //{"code":200,"success":true,"data":["CvUo8ZZ5ZfcBNR7V7GKy2A3Sz1bSfZWrfojWxDmjiBox"],"msg":"操作成功"}
    return res.data
  }).catch((error) => {
    return null
    // return {"code":200,"success":true,"data":["CvUo8ZZ5ZfcBNR7V7GKy2A3Sz1bSfZWrfojWxDmjiBox"],"msg":"操作成功"}
  });
  if (response && response.data && response.data.length >= 0) {
    return response.data[0]
  }
  return ""
}


export const querySolBalance = async (raydium: Raydium) => {
  const balance = await raydium.connection.getBalance(owner.publicKey)
  if (balance) {
    solBalance = new Decimal(balance).div(10 ** 9).toNumber()
    console.log(`用户当前sol余额是: ${solBalance}`)
  }
}

export const queryTokenSupply= async (mint:string)=>  {
  const tokenSupply = await connection.getTokenSupply(new PublicKey(mint));
  console.log("查询代币供应量", tokenSupply)
  if (tokenSupply && tokenSupply.value && tokenSupply.value.uiAmount) {
    mintSupply = Math.floor(tokenSupply.value.uiAmount)
    mintDecimals = tokenSupply.value.decimals
  }
}

export const queryBalance = async (raydium: Raydium, mint: string) => {
// 获取钱包地址的关联token账户地址
  const associatedTokenAddress = await getAssociatedTokenAddress(
      new PublicKey(mint),
      owner.publicKey
  );
  console.log(`Associated Token Address: ${associatedTokenAddress.toBase58()}`)
  // 查询token账户的余额
  const tokenAccount = await raydium.connection.getTokenAccountBalance(associatedTokenAddress).then((result) => {
    console.log(`查询到余额: ${JSON.stringify(result)}`)
    return result;
  }).catch((error) => {
    console.error(`查询余额错误: ${error}`);
  });

  if (tokenAccount) {
    console.log(`USDC Balance: ${tokenAccount.value.uiAmount}`);
    return tokenAccount.value

  } else {
    console.log(`USDC Balance: ${tokenAccount}`);
  }
  return null
}

export const swap = async (raydium: Raydium, amountIn: number, poolId: string) => {
  if (!curProgramId) {
    let data = await raydium.api.fetchPoolById({ ids: poolId });
    if (data && data.length > 0) {
      curProgramId = data[0].programId
    }
  }
  if (curProgramId) {
    if (isValidAmm(curProgramId)) {
      console.info("使用amm交易")
      ammSwap(raydium, amount, poolId)
    } else if (isValidCpmm(curProgramId)) {
      console.info("使用Cpmm交易")
      cpmmSwap(raydium, amount, poolId)
    }  else if (isValidCpmm(curProgramId)) {
      console.info("使用Clmm交易")
      clmmSwap(raydium, amount, poolId)
    } else {
      console.info("不支持的swap类型")
    }
  } else {
    console.log("未找到交易池")
  }
}

export const clmmSwap = async (raydium: Raydium, amountIn: number, poolId: string) => {
  let poolInfo: ApiV3PoolInfoConcentratedItem
  // RAY-USDC pool
  const inputMint = RAYMint.toBase58()
  let poolKeys: ClmmKeys | undefined
  let clmmPoolInfo: ComputeClmmPoolInfo
  let tickCache: ReturnTypeFetchMultiplePoolTickArrays

  const inputAmount = new BN(amountIn)

  if (raydium.cluster === 'mainnet') {
    // note: api doesn't support get devnet pool info, so in devnet else we go rpc method
    // if you wish to get pool info from rpc, also can modify logic to go rpc method directly
    const data = await raydium.api.fetchPoolById({ ids: poolId })
    poolInfo = data[0] as ApiV3PoolInfoConcentratedItem
    if (!isValidClmm(poolInfo.programId)) {
      console.info('target pool is not CLMM pool')
      return
    }

    clmmPoolInfo = await PoolUtils.fetchComputeClmmInfo({
      connection: raydium.connection,
      poolInfo,
    })
    tickCache = await PoolUtils.fetchMultiplePoolTickArrays({
      connection: raydium.connection,
      poolKeys: [clmmPoolInfo],
    })
  } else {
    const data = await raydium.clmm.getPoolInfoFromRpc(poolId)
    poolInfo = data.poolInfo
    poolKeys = data.poolKeys
    clmmPoolInfo = data.computePoolInfo
    tickCache = data.tickData
  }

  if (inputMint !== poolInfo.mintA.address && inputMint !== poolInfo.mintB.address) {
    console.info('input mint does not match pool')
    return
  }

  const baseIn = inputMint === poolInfo.mintA.address

  const { minAmountOut, remainingAccounts } = await PoolUtils.computeAmountOutFormat({
    poolInfo: clmmPoolInfo,
    tickArrayCache: tickCache[poolId],
    amountIn: inputAmount,
    tokenOut: poolInfo[baseIn ? 'mintB' : 'mintA'],
    slippage: slippage,
    epochInfo: await raydium.fetchEpochInfo(),
  })

  const { execute } = await raydium.clmm.swap({
    poolInfo,
    poolKeys,
    inputMint: poolInfo[baseIn ? 'mintA' : 'mintB'].address,
    amountIn: inputAmount,
    amountOutMin: minAmountOut.amount.raw,
    observationId: clmmPoolInfo.observationId,
    ownerInfo: {
      useSOLBalance: true, // if wish to use existed wsol token account, pass false
    },
    remainingAccounts,
    txVersion,

    // optional: set up priority fee here
    // computeBudgetConfig: {
    //   units: 600000,
    //   microLamports: 465915,
    // },

    // optional: add transfer sol to tip account instruction. e.g sent tip to jito
    // txTipConfig: {
    //   address: new PublicKey('96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5'),
    //   amount: new BN(10000000), // 0.01 sol
    // },
  })
  const { txId } = await execute()
  console.log('swapped in clmm pool:', { txId: `https://explorer.solana.com/tx/${txId}` })
  // process.exit() // if you don't want to end up node execution, comment this line
}

export const cpmmSwap = async (raydium: Raydium, amountIn: number, poolId: string) => {
  const inputAmount = new BN(amountIn)
  const inputMint = NATIVE_MINT.toBase58()

  let poolInfo: ApiV3PoolInfoStandardItemCpmm
  let poolKeys: CpmmKeys | undefined
  let rpcData: CpmmRpcData

  if (raydium.cluster === 'mainnet') {
    // note: api doesn't support get devnet pool info, so in devnet else we go rpc method
    // if you wish to get pool info from rpc, also can modify logic to go rpc method directly
    const data = await raydium.api.fetchPoolById({ ids: poolId })
    poolInfo = data[0] as ApiV3PoolInfoStandardItemCpmm
    if (!isValidCpmm(poolInfo.programId)) {
      console.info('target pool is not CPMM pool')
      return
    }
    rpcData = await raydium.cpmm.getRpcPoolInfo(poolInfo.id, true)
  } else {
    const data = await raydium.cpmm.getPoolInfoFromRpc(poolId)
    poolInfo = data.poolInfo
    poolKeys = data.poolKeys
    rpcData = data.rpcData
  }

  if (inputMint !== poolInfo.mintA.address && inputMint !== poolInfo.mintB.address){
    console.info('input mint does not match pool')
    return
  }

  const baseIn = inputMint === poolInfo.mintA.address

  // swap pool mintA for mintB
  const swapResult = CurveCalculator.swap(
      inputAmount,
      baseIn ? rpcData.baseReserve : rpcData.quoteReserve,
      baseIn ? rpcData.quoteReserve : rpcData.baseReserve,
      rpcData.configInfo!.tradeFeeRate
  )

  /**
   * swapResult.sourceAmountSwapped -> input amount
   * swapResult.destinationAmountSwapped -> output amount
   * swapResult.tradeFee -> this swap fee, charge input mint
   */

  const { execute } = await raydium.cpmm.swap({
    poolInfo,
    poolKeys,
    inputAmount,
    swapResult,
    slippage: slippage, // range: 1 ~ 0.0001, means 100% ~ 0.01%
    baseIn,
    // optional: set up priority fee here
    // computeBudgetConfig: {
    //   units: 600000,
    //   microLamports: 4659150,
    // },

    // optional: add transfer sol to tip account instruction. e.g sent tip to jito
    // txTipConfig: {
    //   address: new PublicKey('96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5'),
    //   amount: new BN(10000000), // 0.01 sol
    // },
  })

  printSimulateInfo()
  // don't want to wait confirm, set sendAndConfirm to false or don't pass any params to execute
  const { txId } = await execute({ sendAndConfirm: true })
  console.log(`swapped: ${poolInfo.mintA.symbol} to ${poolInfo.mintB.symbol}:`, {
    txId: `https://explorer.solana.com/tx/${txId}`,
  })

}






export const ammSwap = async (raydium: Raydium, amountIn: number, poolId: string) => {
  // const raydium = await initSdk()
  // const amountIn = 500
  const inputMint = NATIVE_MINT.toBase58()
  // const poolId = '58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2' // SOL-USDC pool

  let poolInfo: ApiV3PoolInfoStandardItem | undefined
  // let poolInfo: any
  let poolKeys: AmmV4Keys | undefined
  let rpcData: AmmRpcData

  if (raydium.cluster === 'mainnet') {
    // note: api doesn't support get devnet pool info, so in devnet else we go rpc method
    // if you wish to get pool info from rpc, also can modify logic to go rpc method directly
    const data = await raydium.api.fetchPoolById({ ids: poolId })
    poolInfo = data[0] as ApiV3PoolInfoStandardItem
    if (!isValidAmm(poolInfo.programId)) {
      console.info('target pool is not AMM pool')
      return
    }
    poolKeys = await raydium.liquidity.getAmmPoolKeys(poolId)
    rpcData = await raydium.liquidity.getRpcPoolInfo(poolId)
  } else {
    // note: getPoolInfoFromRpc method only return required pool data for computing not all detail pool info
    const data = await raydium.liquidity.getPoolInfoFromRpc({ poolId })
    poolInfo = data.poolInfo
    poolKeys = data.poolKeys
    rpcData = data.poolRpcData
  }
  const [baseReserve, quoteReserve, status] = [rpcData.baseReserve, rpcData.quoteReserve, rpcData.status.toNumber()]

  if (poolInfo.mintA.address !== inputMint && poolInfo.mintB.address !== inputMint) {
    console.info('input mint does not match pool')
    return
  }


  const baseIn = inputMint === poolInfo.mintA.address
  const [mintIn, mintOut] = baseIn ? [poolInfo.mintA, poolInfo.mintB] : [poolInfo.mintB, poolInfo.mintA]

  const out = raydium.liquidity.computeAmountOut({
    poolInfo: {
      ...poolInfo,
      baseReserve,
      quoteReserve,
      status,
      version: 4,
    },
    amountIn: new BN(amountIn),
    mintIn: mintIn.address,
    mintOut: mintOut.address,
    slippage: slippage, // range: 1 ~ 0.0001, means 100% ~ 0.01%
  })

  console.log(
    `computed swap ${new Decimal(amountIn)
      .div(10 ** mintIn.decimals)
      .toDecimalPlaces(mintIn.decimals)
      .toString()} ${mintIn.symbol || mintIn.address} to ${new Decimal(out.amountOut.toString())
      .div(10 ** mintOut.decimals)
      .toDecimalPlaces(mintOut.decimals)
      .toString()} ${mintOut.symbol || mintOut.address}, minimum amount out ${new Decimal(out.minAmountOut.toString())
      .div(10 ** mintOut.decimals)
      .toDecimalPlaces(mintOut.decimals)} ${mintOut.symbol || mintOut.address}`
  )

  const { execute } = await raydium.liquidity.swap({
    poolInfo,
    poolKeys,
    amountIn: new BN(amountIn),
    amountOut: out.minAmountOut, // out.amountOut means amount 'without' slippage
    fixedSide: 'in',
    inputMint: mintIn.address,
    txVersion,

    // optional: set up token account
    // config: {
    //   inputUseSolBalance: true, // default: true, if you want to use existed wsol token account to pay token in, pass false
    //   outputUseSolBalance: true, // default: true, if you want to use existed wsol token account to receive token out, pass false
    //   associatedOnly: true, // default: true, if you want to use ata only, pass true
    // },

    // optional: set up priority fee here
    // computeBudgetConfig: {
    //   units: 600000,
    //   microLamports: 46591500,
    // },

    // optional: add transfer sol to tip account instruction. e.g sent tip to jito
    // txTipConfig: {
    //   address: new PublicKey('96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5'),
    //   amount: new BN(10000000), // 0.01 sol
    // },
  })

  printSimulateInfo()
  // don't want to wait confirm, set sendAndConfirm to false or don't pass any params to execute
  const { txId } = await execute({ sendAndConfirm: true })
  console.log(`swap successfully in amm pool:`, { txId: `https://explorer.solana.com/tx/${txId}` })

  //process.exit() // if you don't want to end up node execution, comment this line
}

/** uncomment code below to execute */
// swap()
main()
